package com.guchenbo.example.io;

import cn.hutool.core.util.NumberUtil;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * 文件数据库
 * 使用文件存储数据，为了防止文件过大，实际存储时候将文件拆分成多个小文件写入磁盘，这个称为segment
 * 每个segment文件命名规范，数字.data，数字.index
 * 每个数据在我的这个系统中都有一个唯一序号，序号从1开始，segment文件命名按照顺序依次往上，文件名表示里面存储的数据序号为文件名+1，比如现在有两个segment，分别为：
 * 0.index，序号1～73
 * 73.index，序号74～123
 * 123.index，序号124～160
 * 160.index，序号从161开始
 *
 * @author guchenbo
 * @date 2022/4/29
 */
public class GuFileDb {
    protected static final Logger log = LoggerFactory.getLogger(GuFileDb.class);

    /**
     * 文件大小上限，1KB
     */
    public static final Integer FILE_MAX_SIZE = 1024 * 1024;
    public static final String INDEX_EXT = ".index";
    public static final String DATA_EXT = ".data";
    public static final String NAME_PRE = "gfb-";

    /**
     * 每个索引的长度，固定8个字节，前4个字节表示数据下标，后4个字节表示数据长度，总共两个Integer的长度
     */
    public static final Integer INDEX_LEN_PRE = 8;

    /**
     * 基础目录
     */
    private final String baseDir;

    public GuFileDb(String baseDir) {
        this.baseDir = baseDir;
    }

    /**
     * 文件写入数据
     * 文件的命名idx.index，表示里面的数据从序号为idx+1开始
     *
     * @param subject
     * @param list
     * @throws IOException
     */
    public <T> void writeData(String subject, List<T> list, GuSerializer<T> serializer) throws IOException {
        String dataPath = getDataDirOrCreate(subject);
        int fileIdx = getLatestIdx(dataPath);

        File dataFile = new File(dataPath, buildFileName(fileIdx, DATA_EXT));
        FileOutputStream dataStream = new FileOutputStream(dataFile, true);
        FileOutputStream indexStream = new FileOutputStream(new File(dataPath, buildFileName(fileIdx, INDEX_EXT)),
                        true);

        // 起始大小
        long fileLen = dataFile.length();
        int offset = (int) fileLen;
        // 找出上次的最大的序号
        int lastNo = calLastDataNo(dataPath);
        /*
         * 0.index -> 0、1、2、3，序号从1～4
         * 4.index -> 4、5、6、7、8，序号从5～9
         * 9.index -> 9、10、11，序号从10开始
         *
         * 1、上次最后的序号是last = 12，本次到i=3的时候分段（写入了0、1、2）
         * 9.index >>> 12、13、14
         * 下个文件应该是，15.index = last + i = 12 + 3
         *
         * 2、上次最后的序号是last = 12，本次到i=0的时候分段（没有写入）
         * 9.index >>>
         * 下个文件应该是，12.index = last + i = 12 + 0
         */

        /*
         * 0.index
         *
         * 1、上次最后的序号是last = 0，本次到i=3的时候分段（写入了0、1、2）
         * 0.index >>> 0、1、2
         * 下个文件应该是，3.index = last + i = 0 + 3
         */

        for (int i = 0; i < list.size(); i++) {
            byte[] bs = serializer.serialize(list.get(i));
            int len = bs.length;
            if (offset + len > FILE_MAX_SIZE && offset > 0) {
                // total > 0 保证第一个能写入，不管多大
                // 满了
                int nextIdx = lastNo + i;
                createNewFs(dataPath, nextIdx);
                dataStream.flush();
                indexStream.flush();

                dataStream = new FileOutputStream(new File(dataPath, buildFileName(nextIdx, DATA_EXT)), true);
                indexStream = new FileOutputStream(new File(dataPath, buildFileName(nextIdx, INDEX_EXT)), true);

                // 重置起始大小
                offset = 0;
            }
            dataStream.write(bs);
            indexStream.write(NumberUtil.toBytes(offset));
            indexStream.write(NumberUtil.toBytes(len));
            // 文件大小增加
            offset += len;
        }
        dataStream.flush();
        indexStream.flush();
    }

    /**
     * 获取第几个数据
     *
     * @param subject
     * @param dataNo  数据序号
     * @return
     */
    public <T> T readData(String subject, int dataNo, GuDeserializer<T> deserializer) throws Exception {
        String dataPath = getDataDirOrCreate(subject);
        int fileIdx = findFileIdx(dataPath, dataNo);
        log.info("dataNo {} in file {}", dataNo, fileIdx);
        Preconditions.checkState(fileIdx >= 0, "当前位置数据不存在");

        /*
         * 0.index -> 0、1、2、3，序号从1～4
         * 4.index -> 4、5、6、7、8，序号从5～9
         * 9.index -> 9、10、11，序号从10开始
         */
        int diff = dataNo - fileIdx;
        int offset = (diff - 1) * INDEX_LEN_PRE;

        FileInputStream indexStream = new FileInputStream(new File(dataPath, buildFileName(fileIdx, INDEX_EXT)));
        indexStream.skip(offset);
        byte[] indexByte = new byte[INDEX_LEN_PRE];
        int read = indexStream.read(indexByte);
        Preconditions.checkState(read == INDEX_LEN_PRE, "未读取到完整数据");
        byte[] offsetByte = Arrays.copyOfRange(indexByte, 0, 4);
        byte[] lenByte = Arrays.copyOfRange(indexByte, 4, INDEX_LEN_PRE);

        int dataOffset = NumberUtil.toInt(offsetByte);
        int dataLen = NumberUtil.toInt(lenByte);

        FileInputStream dataStream = new FileInputStream(new File(dataPath, buildFileName(fileIdx, DATA_EXT)));
        byte[] dataByte = new byte[dataLen];
        dataStream.skip(dataOffset);
        read = dataStream.read(dataByte);
        Preconditions.checkState(read == dataLen, "未读取到完整数据");

        indexStream.close();
        dataStream.close();
        return deserializer.deserialize(dataByte);
    }

    /**
     * 上次写入的最后的数据序号，比如
     * 0.index -> 0、1、2、3，序号从1～4
     * 4.index -> 4、5、6、7、8，序号从5～9
     * 9.index -> 9、10、11，序号从10开始
     *
     * @param dataPath
     * @return
     */
    private int calLastDataNo(String dataPath) throws IOException {
        int fileIdx = getLatestIdx(dataPath);
        FileInputStream indexStream = new FileInputStream(new File(dataPath, buildFileName(fileIdx, INDEX_EXT)));
        // 读取最后8个字节，得出上次保存到里第几个数据
        int len = indexStream.available();
        if (len > 0) {
            // 有几个数据
            int cnt = len / INDEX_LEN_PRE;
            return fileIdx + cnt;
        } else {
            return 0;
        }
    }

    /**
     * 数据目录
     *
     * @param subject
     * @return
     */
    private String getDataDirOrCreate(String subject) {
        String dataDir = baseDir + File.separator + subject;
        File dir = new File(dataDir);
        Preconditions.checkState(dir.exists() || dir.mkdirs());
        return dir.getAbsolutePath();
    }

    /**
     * 判断数据序号位于哪个文件
     * 0.index -> 0、1、2、3，序号从1～4
     * 4.index -> 4、5、6、7、8，序号从5～9
     * 9.index -> 9、10、11，序号从10开始
     * 找到文件名比序号小的最大的那个
     *
     * @param dataPath
     * @param dataNo   数据序号
     * @return
     */
    private int findFileIdx(String dataPath, int dataNo) {
        List<Integer> sort = listAndSortIndex(dataPath);
        for (int i = 0; i < sort.size(); i++) {
            if (sort.get(i) >= dataNo) {
                if (i == 0) {
                    return -1;
                } else {
                    return sort.get(i - 1);
                }
            }
        }
        return sort.get(sort.size() - 1);
    }

    private List<Integer> listAndSortIndex(String dataPath) {
        File[] files = listIndexFile(dataPath);
        return sortIndexFile(Lists.newArrayList(files));
    }

    /**
     * 文件名格式：固定前缀 + 8位长度数字（不足前面0补齐，为了更好展示排序） + 指定后缀
     *
     * @param fileIdx
     * @param ext
     * @return
     * @see GuFileDb#NAME_PRE
     */
    private String buildFileName(int fileIdx, String ext) {
        return NAME_PRE + paddingWithZero(fileIdx) + ext;
    }

    private String paddingWithZero(int fileIdx) {
        return String.format("%08d", fileIdx);
    }

    private Integer getLatestIdx(String path) throws IOException {
        List<Integer> sort = listAndSortIndex(path);
        Optional<Integer> max = sort.stream().max(Comparator.naturalOrder());
        if (!max.isPresent()) {
            createNewFs(path, 0);
        }
        return max.orElse(0);
    }

    private File[] listIndexFile(String path) {
        File dir = new File(path);
        Preconditions.checkState(dir.isDirectory(), "路径不是目录");
        File[] files = dir.listFiles(((d, name) -> name.startsWith(NAME_PRE) && name.endsWith(INDEX_EXT)));
        return files == null ? new File[0] : files;
    }

    private List<Integer> sortIndexFile(List<File> files) {
        if (files == null) {
            return Lists.newArrayList();
        }
        return files.stream().map(f -> getFileIdx(f.getName())).sorted().collect(Collectors.toList());
    }

    private int getFileIdx(String filename) {
        return Integer.parseInt(StringUtils.substringAfter(StringUtils.substringBefore(filename, INDEX_EXT), NAME_PRE));
    }

    private void createNewFs(String dir, int fileIdx) throws IOException {
        File dataFile = new File(dir, buildFileName(fileIdx, DATA_EXT));
        File indexFile = new File(dir, buildFileName(fileIdx, INDEX_EXT));
        boolean flag = dataFile.createNewFile() && indexFile.createNewFile();
        Preconditions.checkState(flag);
    }

    public static void main(String[] args) {
        System.out.printf("%08d%n", 12);
        System.out.println(Integer.parseInt("0000012"));
    }

    static class GfsIndex {
        private int offset;
        private int len;

    }
}
